package gis.uzd_1;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.util.HashSet;
import java.util.Set;

import javax.swing.tree.TreePath;

import org.geotools.data.FeatureSource;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.DefaultMapContext;
import org.geotools.map.MapContext;
import org.geotools.renderer.lite.StreamingRenderer;
import org.geotools.styling.FeatureTypeStyle;
import org.geotools.styling.Fill;
import org.geotools.styling.Graphic;
import org.geotools.styling.Mark;
import org.geotools.styling.Rule;
import org.geotools.styling.Stroke;
import org.geotools.styling.Style;
import org.geotools.styling.StyleFactory;
import org.geotools.styling.Symbolizer;
import org.geotools.swing.JMapPane;
import org.geotools.swing.event.MapMouseAdapter;
import org.geotools.swing.event.MapMouseEvent;
import org.geotools.swing.tool.PanTool;
import org.geotools.swing.tool.ZoomInTool;
import org.geotools.swing.tool.ZoomOutTool;

import org.opengis.geometry.Envelope;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.identity.FeatureId;

import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Polygon;

/**
 * Map.
 * 
 * @version 0.1
 * @date 2010-04-19
 * @author Edmundas Matusevicius
 */
public class GISMap extends JMapPane {
    public GISMap() {
        mapContext = new DefaultMapContext();
        
        // set up parameters
        
        setBackground(Color.WHITE);
        setRenderer(new StreamingRenderer());
        setMapContext(mapContext);
        
        // add tools
        
        zoomIn.setMapPane(this);
        zoomIn.setZoom(1.5);
        
        zoomOut.setMapPane(this);
        zoomOut.setZoom(1.5);
        
        pan.setMapPane(this);
        
//        setCursorTool(selectCursor);
        
        setUpActions();
    }
    
    private void setUpActions() {
        addMouseListener(new MapMouseAdapter() {
            public void onMouseClicked(MapMouseEvent e) {
                if (e.getButton() == 1) {
                    if (GISMenu.viewState == GISMenu.ViewState.ZOOM_IN) {
                        zoomIn.onMouseClicked(e);
                    } else if (GISMenu.viewState == GISMenu.ViewState.ZOOM_OUT) {
                        zoomOut.onMouseClicked(e);
                    } else if (GISMenu.viewState == GISMenu.ViewState.SELECT) {
                        // construct 5x5 mouse cursor
                        
                        Point screenPos = e.getPoint();
                        Rectangle screenRect = new Rectangle(screenPos.x - 2, screenPos.y - 2, 5, 5);
                        
                        selectFeatures(screenRect);
                    }
                }
            }
            
            @Override
            public void onMousePressed(MapMouseEvent e) {
                if (e.getButton() == 1) {
                    if (GISMenu.viewState == GISMenu.ViewState.ZOOM_IN) {
                        zoomIn.onMousePressed(e);
                    } else if (GISMenu.viewState == GISMenu.ViewState.MOVE) {
                        pan.onMousePressed(e);
                    } else if (GISMenu.viewState == GISMenu.ViewState.SELECT) {
                        selectCursor.onMousePressed(e);
                    }
                }
            }
            
            @Override
            public void onMouseReleased(MapMouseEvent e) {
                if (e.getButton() == 1) {
                    if (GISMenu.viewState == GISMenu.ViewState.ZOOM_IN) {
                        zoomIn.onMouseReleased(e);
                    } else if (GISMenu.viewState == GISMenu.ViewState.MOVE) {
                        pan.onMouseReleased(e);
                    } else if (GISMenu.viewState == GISMenu.ViewState.SELECT) {
                        selectCursor.onMouseReleased(e);
                        
                        Rectangle screenRect = selectCursor.getBoundsRectangle();
                        selectFeatures(screenRect);
                    }
                }
            }
            
            @Override
            public void onMouseDragged(MapMouseEvent e) {
                if (GISMenu.viewState == GISMenu.ViewState.ZOOM_IN) {
                    zoomIn.onMouseDragged(e);
                } else if (GISMenu.viewState == GISMenu.ViewState.MOVE) {
                    pan.onMouseDragged(e);
                } else if (GISMenu.viewState == GISMenu.ViewState.SELECT) {
                    selectCursor.onMouseDragged(e);
                }
            }
            
            public void onMouseEntered(MapMouseEvent e) {
                if (GISMenu.viewState == GISMenu.ViewState.MOVE) {
                    setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
                } else if (GISMenu.viewState == GISMenu.ViewState.SELECT) {
                    TreePath selectedPath = DataPanel.layerPanel.getSelectionPath();
                    
                    if (selectedPath != null) {
                        if (selectedPath.equals(new TreePath(GISLayer.dataRoot.get(0).getPath()))) {
                            setCursor(Cursor.getDefaultCursor());
                        } else {
                            setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
                        }
                    }
                }
            }
            
            public void onMouseExited(MapMouseEvent e) {
                setCursor(Cursor.getDefaultCursor());
            }
            
            public void onMouseMoved(MapMouseEvent e) {
                String coordinates = "X: " + e.getMapPosition().x + " Y: " + e.getMapPosition().y + "  ";
                StatusPanel.coordLabel.setText(coordinates);
            }
        });
    }
    
    @SuppressWarnings("unchecked")
    public void selectFeatures(Object[] idents) {
        TreePath selectedPath = DataPanel.layerPanel.getSelectionPath();
        
        if (selectedPath != null) {
            if (!selectedPath.equals(new TreePath(GISLayer.dataRoot.get(0).getPath()))) {
                for (int i = 0; i < mapContext.getLayerCount(); i++) {
                    if (selectedPath.getLastPathComponent().toString() == mapContext.getLayer(i).getTitle()) {
                        try {
                            FeatureSource featureSource = mapContext.getLayer(i).getFeatureSource();
                            
                            GeometryDescriptor geometryDesc = featureSource.getSchema().getGeometryDescriptor();
                            geometryAttName = geometryDesc.getLocalName();
                            setGeometry(geometryDesc);
                            
                            Set<FeatureId> IDs = new HashSet<FeatureId>();
                            
                            // use filter to identify selected features
                            try {
                                selectedFeatures = featureSource.getFeatures();
                                FeatureIterator<SimpleFeature> iter = selectedFeatures.features();
                                
                                try {
                                    while (iter.hasNext()) {
                                        SimpleFeature feature = iter.next();
                                        
                                        for (Object ident : idents) {
                                            if (ident.toString().equals(feature.getIdentifier().toString())) {
                                                IDs.add(feature.getIdentifier());
                                                break;
                                            }
                                        }
                                    }
                                } finally {
                                    iter.close();
                                }
                                
                                Filter filter = filterFactory.id(IDs);
                                selectedFeatures = featureSource.getFeatures(filter);
                                selectedBounds = (ReferencedEnvelope) selectedFeatures.getBounds();
                                
                                resetLayers();
                                styleSelectedFeatures(IDs, i);
                            } catch (Exception excep) {
                                excep.printStackTrace();
                            }
                        } catch (Exception exc) {
                            exc.printStackTrace();
                        }
                        
                        break;
                    }
                }
            }
        }
    }
    
    @SuppressWarnings("unchecked")
    private void selectFeatures(Rectangle screenRect) {
        TreePath selectedPath = DataPanel.layerPanel.getSelectionPath();
        
        if (selectedPath != null) {
            if (!selectedPath.equals(new TreePath(GISLayer.dataRoot.get(0).getPath()))) {
                for (int i = 0; i < mapContext.getLayerCount(); i++) {
                    if (selectedPath.getLastPathComponent().toString() == mapContext.getLayer(i).getTitle()) {
                        FeatureSource featureSource = mapContext.getLayer(i).getFeatureSource();
                        
                        // transform mouse pointer rectangle to bounding box in map context
                        
                        AffineTransform screenToWorld = getScreenToWorldTransform();
                        Rectangle2D worldRect = screenToWorld.createTransformedShape(screenRect).getBounds2D();
                        ReferencedEnvelope bbox = new ReferencedEnvelope(worldRect, mapContext.getCoordinateReferenceSystem());
                        
                        GeometryDescriptor geometryDesc = featureSource.getSchema().getGeometryDescriptor();
                        geometryAttName = geometryDesc.getLocalName();
                        setGeometry(geometryDesc);
                        
                        Filter filter = filterFactory.intersects(filterFactory.property(geometryAttName), filterFactory.literal(bbox));
                        
                        Set<FeatureId> IDs = new HashSet<FeatureId>();
                        
                        // use filter to identify selected features
                        try {
                            selectedFeatures = featureSource.getFeatures(filter);
                            FeatureIterator<SimpleFeature> iter = selectedFeatures.features();
                            
                            selectedBounds = (ReferencedEnvelope) selectedFeatures.getBounds();
                            
                            try {
                                while (iter.hasNext()) {
                                    SimpleFeature feature = iter.next();
                                    
                                    IDs.add(feature.getIdentifier());
                                }
                            } finally {
                                iter.close();
                            }
                            
                            if (IDs.isEmpty()) {
                                selectedFeatures = null;
                            }
                            
                            resetLayers();
                            styleSelectedFeatures(IDs, i);
                        } catch (Exception excep) {
                            excep.printStackTrace();
                        }
                        
                        break;
                    }
                }
            }
        }
    }
    
    public void resetLayers() {
        if (selectedFeatures != null) {
            Style style = createDefaultStyle();
            
            for (int i = 0; i < mapContext.getLayerCount(); i++) {
                mapContext.getLayer(i).setStyle(style);
            }
            
            repaint();
        }
    }
    
    private void styleSelectedFeatures(Set<FeatureId> IDs, int layer) {
        Style style;
        
        if (IDs.isEmpty()) {
            style = createDefaultStyle();
            
        } else {
            style = createSelectedStyle(IDs);
        }
        
        mapContext.getLayer(layer).setStyle(style);
        
        repaint();
    }
    
    private Style createDefaultStyle() {
        Rule rule = createRule(null, null);
        
        FeatureTypeStyle fts = styleFactory.createFeatureTypeStyle();
        fts.rules().add(rule);
        
        Style style = styleFactory.createStyle();
        style.featureTypeStyles().add(fts);
        
        return style;
    }
    
    private Style createSelectedStyle(Set<FeatureId> IDs) {
        Rule selectedRule = createRule(SELECTED_LINE_COLOUR, SELECTED_FILL_COLOUR);
        selectedRule.setFilter(filterFactory.id(IDs));
        
        Rule otherRule = createRule(null, null);
        otherRule.setElseFilter(true);
        
        FeatureTypeStyle fts = styleFactory.createFeatureTypeStyle();
        fts.rules().add(selectedRule);
        fts.rules().add(otherRule);
        
        Style style = styleFactory.createStyle();
        style.featureTypeStyles().add(fts);
        
        return style;
    }
    
    private Rule createRule(Color outlineColor, Color fillColor) {
        Symbolizer symbolizer = null;
        Fill fill = null;
        Stroke stroke = styleFactory.createStroke(filterFactory.literal(outlineColor), filterFactory.literal(LINE_WIDTH));
        
        switch (geometryType) {
            case POLYGON:
                fill = styleFactory.createFill(filterFactory.literal(fillColor), filterFactory.literal(OPACITY));
                symbolizer = styleFactory.createPolygonSymbolizer(stroke, fill, geometryAttName);
                break;
                
            case LINE:
                symbolizer = styleFactory.createLineSymbolizer(stroke, geometryAttName);
                break;
                
            case POINT:
                fill = styleFactory.createFill(filterFactory.literal(fillColor), filterFactory.literal(OPACITY));
                
                Mark mark = styleFactory.getCircleMark();
                mark.setFill(fill);
                mark.setStroke(stroke);
                
                Graphic graphic = styleFactory.createDefaultGraphic();
                graphic.graphicalSymbols().clear();
                graphic.graphicalSymbols().add(mark);
                graphic.setSize(filterFactory.literal(POINT_SIZE));
                
                symbolizer = styleFactory.createPointSymbolizer(graphic, geometryAttName);
        }
        
        Rule rule = styleFactory.createRule();
        rule.symbolizers().add(symbolizer);
        return rule;
    }
    
    private void setGeometry(GeometryDescriptor geomDesc) {
        Class<?> clazz = geomDesc.getType().getBinding();
        
        if (Polygon.class.isAssignableFrom(clazz) ||
                MultiPolygon.class.isAssignableFrom(clazz)) {
            geometryType = GeomType.POLYGON;
        } else if (LineString.class.isAssignableFrom(clazz) ||
                MultiLineString.class.isAssignableFrom(clazz)) {
            geometryType = GeomType.LINE;
        } else {
            geometryType = GeomType.POINT;
        }
    }
    
    public void showSelectedFeatures() {
        if (selectedBounds != null)
            setDisplayArea(selectedBounds);
    }
    
    // selected features
    
    @SuppressWarnings("unchecked")
    static FeatureCollection selectedFeatures = null;
    
    // map selection bounds
    
    private Envelope selectedBounds = null;
    
    // map styling
    
    private String geometryAttName = null;
    private GeomType geometryType;
    private enum GeomType { POINT, LINE, POLYGON };
    
    private static final Color SELECTED_LINE_COLOUR = Color.RED;
    private static final Color SELECTED_FILL_COLOUR = Color.PINK;
    
    private static final float OPACITY = 1.0f;
    private static final float LINE_WIDTH = 1.0f;
    private static final float POINT_SIZE = 10.0f;
    
    // map data
    
    static MapContext mapContext = null;
    
    // factories
    
    private StyleFactory styleFactory = CommonFactoryFinder.getStyleFactory(null);
    private FilterFactory2 filterFactory = CommonFactoryFinder.getFilterFactory2(null);
    
    // map manipulation tools
    
    private ZoomInTool zoomIn = new ZoomInTool();
    private ZoomOutTool zoomOut = new ZoomOutTool();
    private PanTool pan = new PanTool();
    private SelectCursorTool selectCursor = new SelectCursorTool();
}
